Alpha Anywhere Service Endpoint Definition

Description

Files with the .a5svc extension is used to describe how an xbasic or node class or api is exposed as a REST service.

Top Level Defaults Pattern to use for API

The top level fields in the service definition file include the defaults for pattern of HTTP REST endpoints to use.

Contains multiple APIs and an optional authentication.

Name
Property and Description
use_query

Default behavior for method endpoints - use query parameters to store arguments.

flatten_arguments

Omit the 'arguments.' prefix from arguments passed on command line or in posted data.

path

Default path template.

Template
Description
{service}

Placeholder for 'service' name.

{method}

Placeholder for 'method' name.

{properties.*}

Placeholder for property '*' is the name of the class instance property to set .

{arguments.*}

Placeholder for argument '*' is the name of the method argument to set .

api

Array of all the xbasic classes and/or node apis that endpoint exposes.

auth_type

Array of all the auth types used by this service (see Authorize Definition), names are required for entries.

auth

Global Xbasic class or node api that implements "Authorize" method for all api entries (see Authorize Definition).

debugger_header

If set, enable debugging of the endpoint using the agent.

API definition

The Api definition level defines the xbasic classes and node apis to expose as endpoints.

API level definition can include default patterns to use to expose methods.

Name
Property and Description
implementation

Implementation of Service, "xbasic" and "node".

service

Name of node service api or xbasic class to expose as an api.

use_query

Default behavior for method endpoints on this API - use query parameters to store arguments. Implies 'GET' method.

flatten_arguments

Omit the 'arguments.' prefix from arguments passed on command line or in posted data.

allow

If API defines method overrides, fallback to the default pattern if method override is not defined (default to true).

allow_empty

Set to false if path parameters must match at least one character - i.e. for a path /account/{id} , id would need to have at least one character.

path

Default path template.

Template
Description
{method}

Placeholder for 'method' name.

{properties.*}

Placeholder for property '*' is the name of the class instance property to set .

{arguments.*}

Placeholder for argument '*' is the name of the method argument to set .

methods

Method overrides to the default pattern.

Method override definition

Method overrides allow definition of pattern to use for individual methods on a xbasic class or node api.

Name
Property and Description
name

Name of method to override.

path

Path template.

Template
Description
{properties.*}

Placeholder for property '*' is the name of the class instance property to set .

{arguments.*}

Placeholder for argument '*' is the name of the method argument to set .

method

HTTP method to use for this endpoint (i.e. GET/POST/PUT/DELETE/PATCH).

use_query

Behavior for this endpoints - use query parameters to store arguments.

flatten_arguments

Omit the 'arguments.' prefix from arguments passed on command line or in posted data.

single_argument

For methods that have a single argument, this specifies the name of this argument, and places the content of the request into it.

argument_mapping

Override argument names for this method

body_encoding

Set the encoding for the body when data is POST or PUT, defaults to json if ommitted.

header_arguments

Define which input parameters are passed in HTTP headers.

Template
Description
header_key_name

Name of the header 'key'.

argument_template

Template to populate elements from.

Template
Description
{properties.*}

Placeholder for property '*' is the name of the class instance property to set .

{arguments.*}

Placeholder for argument '*' is the name of the method argument to set .

binary

Determine how binary content passed to method is handled for the request (see Binary types)

response_binary

Determine how binary content returned from method is handled for the request (see Binary types)

auth

Name of auth type to use for this method.

Authorize Definition

Authorize definition provide the binding of the API call to handle the authentication of the request.

Name
Property and Description
name

Name of the Authorization.

implementation

Implementation of Authorize method, "xbasic" and "node".

service

Name of node service api or xbasic class that implements the Authorize method.

authorize

Override the name of the method used to authorize (will assume the method is called 'Authorize' otherwise).

path

Path template.

Template
Description
{properties.*}

Placeholder for property '*' is the name of the class instance property to set .

{arguments.*}

Placeholder for argument '*' is the name of the method argument to set .

use_query

Behavior for this endpoints - use query parameters to store arguments (i.e. if api_key is passed as a query parameter).

flatten_arguments

Omit the 'arguments.' prefix from arguments passed on command line or in posted data.

single_argument

For methods that have a single argument, this specifies the name of this argument, and places the content of the request into it.

argument_mapping

Override argument names for this method

header_arguments

Define which input parameters are passed in HTTP headers.

Template
Description
header_key_name

Name of the header 'key'.

argument_template

Template to populate elements from.

Template
Description
{properties.*}

Placeholder for property '*' is the name of the class instance property to set .

{arguments.*}

Placeholder for argument '*' is the name of the method argument to set .

binary

Binary Types

CORS Definition

Specifies Cross Origin Resource Sharing settings for the endpoint.

headers

String containing comma separated List of allowed headers, use '*' for allow all requested headers.

Binary Types

Name
Property and Description
multipart

Binary content passed as multipart form elements.

raw

Binary content passed as raw content (requires a single binary return value).

base64

Binary content inline as base64 encoded text.

bson

Binary content inline bson format (JSON representation of BSON binary record).

array

Binary content represented as inline byte arrays.

javascript

Binary content represented as jaavscript.

Example MongoDb Collection exposed as a REST API

A class that defines methods to get and add, update and delete records from a mongo database, using the extension::MongoDB class.

define class restapi::customer
	function GetCustomers as p()
		dim Mongo as extension::MongoDB = extension::MongoDB::Create("mongodb://localhost:27017","alphasports","Customer")
		GetCustomers = json_parse(mongo.GetRecords(""))
	end function
	
	function GetCustomer as p(id as c)
		dim Mongo as extension::MongoDB = extension::MongoDB::Create("mongodb://localhost:27017","alphasports","Customer")
		dim json as c = mongo.GetRecords("{ \"CUSTOMER_ID\" : \""+id+"\" }")
		if json <> "" then
			dim jso as extension::JSON
			jso.setJson(json)
			if jso.getLength() > 0 then
				dim jsi as extension::JSON = jso.getIndexed(0)
				GetCustomer = json_parse(jsi.getJson())
			end if
		end if
	end function
	
	function AddCustomer as l(obj as p)
		if obj.data("CUSTOMER_ID") <> "" then
			dim json as c = json_generate(obj)
			dim Mongo as extension::MongoDB = extension::MongoDB::Create("mongodb://localhost:27017","alphasports","Customer")
			Mongo.AddRecord(json)
			AddCustomer = .t.
		end if	
	end function
	
	function DeleteCustomer as l(id as c)
		dim obj as p = GetCustomer(id)
		dim _id as c = obj.data("_id")
		if _id <> "" then
			dim Mongo as extension::MongoDB = extension::MongoDB::Create("mongodb://localhost:27017","alphasports","Customer")
			mongo.DeleteRecord(_id)
			DeleteCustomer = .t.
		end if	
	end function
	
	function UpdateCustomer as l(newObj as p)
		dim id as c = newObj.data("CUSTOMER_ID")
		if id <> "" then
			dim obj as p = GetCustomer(id)
			dim _id as c = obj.data("_id")
			if _id <> "" then
				property_recurse_assign(obj,newObj)				
				dim json as c = json_generate(obj)
				dim Mongo as extension::MongoDB = extension::MongoDB::Create("mongodb://localhost:27017","alphasports","Customer")
				Mongo.UpdateRecord(_id,json)
				UpdateCustomer = .t.
			end if	
		end if	
	end function
end class

Definition file for class

Example Definition for mapping the REST api to a service endpoint.

{
    "path": "/{method}",
    "method": "get",
    "api": [
        {
            "implementation": "xbasic",
            "service": "restapi::customer",
            "methods": [
                {
                    "name": "AddCustomer",
                    "path": "/AddCustomer",
                    "method": "post",
                    "single_argument" : "obj"
                },
                {
                    "name": "DeleteCustomer",
                    "path": "/DeleteCustomer/{arguments.id}",
                    "method": "delete"
                },
                {
                    "name": "GetCustomer",
                    "path": "/GetCustomer/{arguments.id}",
                    "method": "get"
                },
                {
                    "name": "GetCustomers",
                    "path": "/GetCustomers",
                    "method": "get"
                },
                {
                    "name": "UpdateCustomer",
                    "path": "/UpdateCustomer",
                    "method": "put",
                    "single_argument" : "newObj"
                }
            ]
        }
    ]
}

Example CURL calls to Endpoint

Get All Customer Records.

c:\>curl http://127.0.0.1:1580/LivePreview/customer.a5svc/GetCustomers
[
  	{
  		"_id": "55b2567b5590795052a1d171",
  		"CUSTOMER_ID": "1000",
  		"FIRSTNAME": "Cecelia",
  		"LASTNAME": "DeLuca",
  		"COMPANY": "Cartwright Industries",
  		"PHONE": null,
  		"FAX": null,
        ...

Add a new Customer record

c:\>curl --data "{ \"CUSTOMER_ID\" : \"99\" , \"FIRSTNAME\" : \"John\" }" http://127.0.0.1:1580/LivePreview/customer.a5svc/AddCustomer
true

Get the record that was added, given the id used in CUSTOMER_ID.

c:\>curl http://127.0.0.1:1580/LivePreview/customer.a5svc/GetCustomer/99
{
        "_id": "59a31d36bc16f60290ba7f17",
        "CUSTOMER_ID": "99",
        "FIRSTNAME": "John"
  }

Delete the Customer Record that was created.

c:\>curl -X DELETE http://127.0.0.1:1580/LivePreview/customer.a5svc/DeleteCustomer/99
true

Example usage of 'Auth'.

In this example, we will have two classes, one that implements methods to call, which for this example is a class with a single method called greet.

define class noisy::talker
    function greet as c(name as c)
		'DESCRIPTION:This is a function that greets the caller
		'ARGUMENT(name):The name of the guy
		greet = "Hello "+name
	end function
end class

Another class will implement a well known method called 'Authorize', which is called for definitions that include the optional 'auth' section. The Authorize method returns the HTTP code - if it returns a successful code (i.e. 200) the implementation class method (greet in this case) will get called, otherwise the result code gets immediately returned.

define class noisy::authorize
    function Authorize as n(api_key as c)
    	if api_key = "bingo" then
    		Authorize = 200
    	else
    	    Authorize = 403
    	end if
    end function
end class

The definition enables the Authorization by defining an 'auth' at the top level that includes the class that implements authorization.

{
    "path": "/{method}",
    "use_query": true,
    "flatten_arguments": true,
    "debugger_header": true,
    "method": "get",
    "auth" : { 
      "implementation": "xbasic",
      "service": "noisy::authorize",
      "header_arguments" : [ { "header_key_name" : "api_key", "argument_template" : "{arguments.api_key}" } ]
     },
    "api": [
        {
            "implementation": "xbasic",
            "service": "noisy::talker",
            "methods": [
                {
                    "name": "greet",
                    "path": "/greet/{arguments.name}",
                    "method": "get"
                }
            ]
        }
    ]
}

Calling the endpoint without the 'magic' api_key header value will result in a Forbidden call.

c:\>curl -X GET --header "Accept: text/html" "http://127.0.0.1:1580/LivePreview/talker.a5svc/greet/fred" --header "api_key: badkey"
Forbidden
c:\>curl -X GET --header "Accept: text/html" "http://127.0.0.1:1580/LivePreview/talker.a5svc/greet/fred" --header "api_key: bingo"
"Hello fred"

Example usage of 'Auth_Type'.

In this example, A class defines two different auth methods.

define class A5RESTImpl::MultiAuth
	function authUser as n(api_key as c)
		if api_key = "bingo" .or. api_key = "souperyouser" then
			authUser = 200
		else	
			authUser = 401
		end if
	end function
	function authAdmin as n(api_key as c)
		if api_key = "souperyouser" then
			authAdmin = 200
		else	
			authAdmin = 401
		end if
	end function
	function ListUsers as p()
		if file.exists("c:\data\users.json") then
			ListUsers  = json_parse(file.to_string("c:\data\users.json"))			
			if ListUsers.size() > 0 then
				dim cleanup as c
				for i = ListUsers.size() to 1 step -1
					if ListUsers[i].data("deleted") then
						ListUsers.delete(i)
						cleanup = .t.
					end if
				next
				if cleanup then
					file.from_string( "c:\data\users.json" , json_reformat(json_generate(ListUsers)) )
				end if
			end if 	
		else	
			ListUsers  = json_parse("[]")
		end if
	end function
	function GetUserInfo as p(id as c)
		dim json as c = extension::json::FileJSONGet("c:\data\users.json","{ \"id\" : \""+id+"\" }")
		if json <> "" then
			GetUserInfo = json_parse(json)
		end if
	end function
	function AddUser as p(user as A5RESTImpl::MultiAuth::User)
		if user.id = "" then
			AddUser = json_parse(json_sanitize("{ added : false , error : 'Id is required' }"))
		else if extension::json::FileJSONGet("c:\data\users.json","{ \"id\" : \""+user.id+"\" }") = "" then			
			extension::json::FileJSONAppend("c:\data\users.json",json_generate(user))
			AddUser = json_parse(json_sanitize("{ added : true }"))
		else	
			AddUser = json_parse(json_sanitize("{ added : false , error : 'User Already Exists' }"))
		end if
	end function
	function UpdateUserInfo as p(user as A5RESTImpl::MultiAuth::User)
		if extension::json::FileJSONGet("c:\data\users.json","{ \"id\" : \""+user.id+"\" }") = "" then			
			AddUser = json_parse(json_sanitize("{ updated : false , error : 'User Not Found' }"))
		else	
			extension::json::FileJSONUpdate("c:\data\users.json","{ \"id\" : \""+user.id+"\" }",json_generate(user))
			AddUser = json_parse(json_sanitize("{ updated : true }"))
		end if
	end function
	function DeleteUserInfo as p(id as c)
		if extension::json::FileJSONGet("c:\data\users.json","{ \"id\" : \""+id+"\" }") = "" then
			AddUser = json_parse(json_sanitize("{ deleted : false , error : 'User Not Found' }"))
		else
			extension::json::FileJSONUpdate("c:\data\users.json","{ \"id\" : \""+id+"\" }","{ \"deleted\" : true } ")
			AddUser = json_parse(json_sanitize("{ deleted : true }"))
		end if	
	end function
end class       	

define class A5RESTImpl::MultiAuth::User
   dim id as c
   dim firstname as c
   dim lastname as c
end class

The example a5svc file for this service defines auth_types of user and admin. The methods in the interface name the method that they use (auth property of method).

{
    "method": "get",
    "allow": false,
    "auth_type" : [
	    {
	        "name" : "user",
            "implementation": "xbasic",
            "service": "A5RESTImpl::MultiAuth",
            "authorize": "authUser",
            "use_query" : true
 	    },
	    {
	        "name" : "admin",
            "implementation": "xbasic",
            "service": "A5RESTImpl::MultiAuth",
            "authorize": "authAdmin",
            "use_query" : true
 	    }
    ],
    "api": [
        {
            "implementation": "xbasic",
            "service": "A5RESTImpl::MultiAuth",
            "methods": [
                {
                    "name": "AddUser",
                    "method": "post",
                    "path": "/AddUser",
                    "use_query": false,
                    "auth" : "admin"
                },
                {
                    "name": "DeleteUserInfo",
                    "method": "delete",
                    "path": "/DeleteUserInfo/{arguments.id}",
                    "use_query": false,
                    "auth" : "admin"
                },
                {
                    "name": "GetUserInfo",
                    "method": "get",
                    "path": "/GetUserInfo/{arguments.id}",
                    "use_query": false,
                    "auth" : "user"
                },
                {
                    "name": "ListUsers",
                    "method": "get",
                    "path": "/ListUsers",
                    "use_query": false,
                    "auth" : "user"
                },
                {
                    "name": "UpdateUserInfo",
                    "method": "put",
                    "path": "/UpdateUserInfo",
                    "use_query": false,
                    "auth" : "admin"
                }
            ]
        }
    ],
    "flatten_arguments": true,
    "debugger_header": false
}

Calling the api, notice that the api_key required for access varies between methods. DeleteUserInfo requires a different key than the ListUsers endpoint.

c:\>curl -X GET "http://127.0.0.1:1580/LivePreview/MultiAuth.a5svc/ListUsers" -H "accept: application/json" -H "content-type: application/json"
Unauthorized
c:\>curl -X GET "http://127.0.0.1:1580/LivePreview/MultiAuth.a5svc/ListUsers?api_key=bingo" -H "accept: application/json" -H "content-type: application/json"
[
        {
                "id": "jqp",
                "firstname": "John",
                "lastname": "Public"
        },
        {
                "id": "js",
                "firstname": "Joe",
                "lastname": "Shmoe"
        }
]
c:\>curl -X DELETE "http://127.0.0.1:1580/LivePreview/MultiAuth.a5svc/DeleteUserInfo/js?api_key=bingo" -H "accept: application/json" -H "content-type: application/json"
Unauthorized
c:\>curl -X DELETE "http://127.0.0.1:1580/LivePreview/MultiAuth.a5svc/DeleteUserInfo/js?api_key=souperyouser" -H "accept: application/json" -H "content-type: application/json"
{
}
c:\>curl -X GET "http://127.0.0.1:1580/LivePreview/MultiAuth.a5svc/ListUsers?api_key=bingo" -H "accept: application/json" -H "content-type: application/json"
[
        {
                "id": "jqp",
                "firstname": "John",
                "lastname": "Public"
        }
]

CORS settings

Cross Origin Resource Sharing is an important issue for services, especially since services generally are called from different hosts.

There is a "cors" section to the A5SVC definition which controls which headers are allowed in a request. If you set the "headers" to "*", then the endpoint will allow every header that the caller requests.

{
    ....
    "cors" : {
        "headers" : "*"
    }
}

Custom CORS behaviour

A custom handler can be defined for options if anything more complicated is required.

The service definition 'options_handler' section contains the name of the class, the language used, and the name of the class method to call.

{
    ...
    "options_handler": {
        "service": "customOptions::Manager",
        "implementation": "xbasic",
        "name": "option_request_handler",
    }
    ...
}

In the above example, the implementation is a function with no arguments that sets the __http_status that is to be returned (you can return 401 if you don't like the request). In this example we are adding headers to allow content-type and apikey headers.

define class customOptions::Manager
    dim __http_status as n 
     ....
    function option_request_handler as n ()
        'handle the browser's pre-flight OPTIONS request
        self.__http_status = 200
        Response.AddHeader("Access-Control-Allow-Headers: content-type,apikey")		
    end function
    ...
end class